/*
 * Copyright 2013 Clareity Security, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.clareitysecurity.shibboleth.db;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
import java.util.List;

import net.clareitysecurity.shibboleth.storage.SessionID;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.clareity.shibboleth.db.CsIdpSession;

import edu.internet2.middleware.shibboleth.idp.session.impl.SessionManagerEntry;

public class SessionObjectDAOImpl {
	private static final Logger log = LoggerFactory.getLogger(SessionObjectDAOImpl.class);
	
	public static final String version = "Version 1.0.3 -- 2013-08-27";

	public SessionObjectDAOImpl() {
		log.debug(version);
	}

	/**
	 * Put a session object into the persistent storage of the database.
	 * 
	 * @param index
	 * @param object
	 * @return
	 */
	public static Object putSession(String systemName, String index, Object object) {

		try {

			// the first thing to do is see if this session object already exists
			CsIdpSession persistSession = getCsIdpSession(systemName, index);
			if (persistSession == null) {
				log.debug("No session object found, performing insert on [{}].", index);
				persistSession = new CsIdpSession();
				persistSession.setObjectIndex(index);
				persistSession.setCreateDate(new Date());
			}
			persistSession.setModifyDate(new Date());

			if (object instanceof SessionManagerEntry) {
				// pull out the login id and expire time when storing
				SessionManagerEntry sme = (SessionManagerEntry) object;
				String loginId = sme.getSession().getPrincipalName();
				DateTime expireTime = sme.getExpirationTime();
				// object converted to sme and stored as mediumblob in db
				// remaining properties of shibboleth session need to be loaded before merge to db
				try {
					ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
					ObjectOutputStream objOut = new ObjectOutputStream(byteOut);
					objOut.writeObject(sme);
					objOut.close();
					byteOut.close();
					byte[] bytes = byteOut.toByteArray();
					persistSession.setObjectBlob(bytes);
					persistSession.setSystemName(systemName);
					persistSession.setLoginId(loginId);
					persistSession.setSessionId(sme.getSessionId());
					persistSession.setSessionExpireTime(expireTime.toDate());
					org.hibernate.Session hibSession = openSession();
					CsIdpSession s = (CsIdpSession) hibSession.merge(persistSession);
					log.debug("New persistent session save to db [{}]", s.getLoginId());
					closeSession(hibSession);
					return s;
				} catch (IOException ioe) {
					log.error("", ioe);
				}

				log.error("Failed storing [{}] to persistent storage.", index);
				return null;
			}

			if (object instanceof SessionID) {
				// we won't have a login id or expire time, just a session id value
				SessionID sid = (SessionID) object;
				// object converted to sme and stored as mediumblob in db
				// remaining properties of shibboleth session need to be loaded before merge to db
				try {
					ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
					ObjectOutputStream objOut = new ObjectOutputStream(byteOut);
					objOut.writeObject(sid);
					objOut.close();
					byteOut.close();
					byte[] bytes = byteOut.toByteArray();
					persistSession.setObjectBlob(bytes);
					persistSession.setSystemName(systemName);
					persistSession.setSessionId(sid.sessionID);
					org.hibernate.Session hibSession = openSession();
					CsIdpSession s = (CsIdpSession) hibSession.merge(persistSession);
					log.debug("New persistent session save to db [{}]", s.getLoginId());
					closeSession(hibSession);
					return s;
				} catch (IOException ioe) {
					log.error("", ioe);
				}

				log.error("Failed storing [{}] to persistent storage.", index);
				return null;
			}

			// Don't know about the object type, so just store it
			try {
				ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
				ObjectOutputStream objOut = new ObjectOutputStream(byteOut);
				objOut.writeObject(object);
				objOut.close();
				byteOut.close();
				byte[] bytes = byteOut.toByteArray();
				persistSession.setObjectBlob(bytes);
				persistSession.setSystemName(systemName);
				org.hibernate.Session hibSession = openSession();
				CsIdpSession s = (CsIdpSession) hibSession.merge(persistSession);
				log.debug("New persistent session save to db [{}]", s.getLoginId());
				closeSession(hibSession);
				return s;
			} catch (IOException ioe) {
				log.error("", ioe);
			}

			log.error("Failed storing [{}] to persistent storage.", index);
			return null;

		} catch (Exception e) {
			log.error("Exception while saving session information to database.");
			log.error("", e);
			return null;
		}
	}

	public static Object getSession(String systemName, String index) {
		try {
			CsIdpSession s = (CsIdpSession) getCsIdpSession(systemName, index);
			
			if (s == null) {
				log.debug("No matching idp session object found for [{}] [{}]", systemName, index);
				return null;
			}
			
			ByteArrayInputStream byteIn = new ByteArrayInputStream(s.getObjectBlob());
			ObjectInputStream in = new ObjectInputStream(byteIn);
			Object obj = in.readObject();
			in.close();

			return obj;
		} catch (IOException ioe) {
			log.error("", ioe);
		} catch (Exception cnfe) {
			log.error("", cnfe);
		}

		return null;
	}

	public static Object removeSession(String systemName, String index) {
		try {
			CsIdpSession s = (CsIdpSession) getCsIdpSession(systemName, index);
			
			if (s == null) {
				log.debug("No matching idp session object found for [{}] [{}]", systemName, index);
				return null;
			}

			boolean b = delete(s);
			if (b == false) {
				log.error("Failed to delete session object [{}] [{}]", systemName, index);
				return null;
			}

			ByteArrayInputStream byteIn = new ByteArrayInputStream(s.getObjectBlob());
			ObjectInputStream in = new ObjectInputStream(byteIn);
			Object obj = in.readObject();
			in.close();

			if (obj instanceof SessionManagerEntry) {
				// since the session object is being deleted, we need to delete the child
				// session objects as well, anything with a matching session id for the given
				// system name
				SessionManagerEntry sme = (SessionManagerEntry) obj;
				deleteBySessionId(systemName, sme.getSessionId());
			}
			return obj;
		} catch (Exception e) {
			log.error("", e);
		}

		return null;
	}

	public static boolean delete(CsIdpSession is) {
		log.debug("Deleting session [{}]/[{}]", is.getSystemName(), is.getObjectIndex());
		try {
			// open the session
			Session session = openSession();
			// delete the object from the db.
			session.delete(is);
			// close the session
			closeSession(session);
			// return the return value
			return true;
		} catch (Exception he) {
			log.error("Exception deleting idp session record [" + is.getUid() + "]", he);
			return false;
		}
	}

	
	public static List<CsIdpSession> getExpiredSessions(String systemName) {
		log.debug("Searching for expired sessions in system [{}]", systemName);

		try {
			DateTime expireTime = new DateTime();
			// open the session
			Session session = openSession();
			// query
			Query q = session.createQuery("from CsIdpSession t where t.systemName = :systemname and t.sessionExpireTime < :expiretime");
			q.setString("systemname", systemName);
			q.setTimestamp("expiretime", expireTime.toDate());
			// get the list of returned records
			List<CsIdpSession> sessionList = q.list();
			// close the session
			closeSession(session);
			// return the found CsAuthenticator
			return sessionList;

		} catch (Exception e) {
			log.error("Error searching for session objects");
			log.error("", e);
			return null;
		}

	}

	
	/**
	 * Get the Hibernate CsIdpSession object given the parameters.
	 * 
	 * @param systemName
	 * @param index
	 * @return
	 */
	private static CsIdpSession getCsIdpSession(String systemName, String index) {
		log.debug("Searching for [{}] in system [{}]", index, systemName);

		try {

			CsIdpSession retVal = null;
			// open the session
			Session session = openSession();
			// query
			Query q = session.createQuery("from CsIdpSession t where t.systemName = :systemname and t.objectIndex = :index");
			q.setString("systemname", systemName);
			q.setString("index", index);
			// get the list of returned records
			List<CsIdpSession> sessionList = q.list();
			// if there's more than NONE
			if (sessionList.size() > 0) {
				retVal = (CsIdpSession) sessionList.get(0);
			}
			// close the session
			closeSession(session);
			// return the found CsAuthenticator
			return retVal;

		} catch (Exception e) {
			log.error("Error searching for existing session object");
			log.error("", e);
			return null;
		}

	}

	/**
	 * Delete any matching rows with the same session ID and systemName
	 * @param systemName
	 * @param sessionId
	 * @return
	 */
	private static boolean deleteBySessionId(String systemName, String sessionId) {
		log.debug("Deleting by [{}] in system [{}]", sessionId, systemName);

		try {
			// open the session
			Session session = openSession();
			// query for any matching records
			Query q = session.createQuery("from CsIdpSession t where t.systemName = :systemname and t.sessionId = :sessionid");
			q.setString("systemname", systemName);
			q.setString("sessionid", sessionId);
			// get the list of returned records
			List<CsIdpSession> sessionList = q.list();
			// if there's more than NONE
			for (CsIdpSession idpSession: sessionList) {
				delete(idpSession); // delete each one
			}
			// close the session
			closeSession(session);
			return true;
		} catch (Exception e) {
			log.error("Error deleting existing session objects. [{}]", sessionId);
			log.error("", e);
			return false;
		}

	}

	/**
	 * Hibernate utility methods and classes
	 * 
	 */

	public static Session openSession() {
		SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
		Session session = sessionFactory.openSession();
		session.beginTransaction();
		log.trace("session opened [{}]", session.toString());
		return session;
	}

	public static Session openSession(SessionFactory sessionFactory) {
		Session session = sessionFactory.openSession();
		session.beginTransaction();
		log.trace("session opened [{}]", session.toString());
		return session;
	}

	public static void closeSession(Session session) {
		session.getTransaction().commit();
		session.close();
		log.trace("session closed [{}]", session.toString());
	}

	private static class HibernateUtil {
		private static final SessionFactory sessionFactory;
		/** Class logger. */
		private static final Logger log = LoggerFactory.getLogger(HibernateUtil.class);

		static {
			try {
				// Create the SessionFactory from standard (hibernate.cfg.xml)
				// config file.
				sessionFactory = new Configuration().configure().buildSessionFactory();
			} catch (Throwable ex) {
				// Log the exception.
				System.err.println("Initial SessionFactory creation failed." + ex);
				log.error("Exception occured creating SessionFactory", ex);
				throw new ExceptionInInitializerError(ex);
			}
		}

		public static SessionFactory getSessionFactory() {
			return sessionFactory;
		}

		public static Session getSession() {
			return sessionFactory.getCurrentSession();
		}
	}

	// public static SessionManagerEntry merge(String id, SessionManagerEntry obj) {
	// return obj;
	//
	// }
	//
	// public static SessionManagerEntry getSessionObject(String id) {
	// SessionManagerEntry obj;
	//
	// return null;
	// }
}
